/* Copyright (c) 2015,2016 Lucky Byte, Inc.
 */
package com.lucky_byte.pay.jar;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.jse.JsePlatform;

import com.lucky_byte.pay.jar.lualib.LuaGB18030;
import com.lucky_byte.pay.jar.lualib.LuaJdbc;
import com.lucky_byte.pay.jar.lualib.LuaLib071;
import com.lucky_byte.pay.jar.lualib.LuaLib091;
import com.lucky_byte.pay.jar.lualib.LuaLogger;
import com.lucky_byte.pay.jar.lualib.LuaMail;
import com.lucky_byte.pay.jar.lualib.LuaRuntime;

/**
 * 嵌入 Lua 解释器
 */
public final class LuaInterp
{
	private static final Logger logger = LogManager.getLogger();

	private final Globals globals;
	private long elapsed_time;

	/**
	 * 构造器
	 */
	public LuaInterp() {
		this.globals = JsePlatform.debugGlobals();

		// 修改 LUAPATH
		String lua_path = getLuaPath();
		String luaj_luapath = System.getProperty("luaj.package.path");
		if (luaj_luapath != null) {
			lua_path += luaj_luapath;
		}
		this.globals.package_.setLuaPath(lua_path);

		// 这些库在所有脚本环境都是可用的
		this.globals.load(new LuaLogger());
		this.globals.load(new LuaRuntime(this));
		this.globals.load(new LuaJdbc());
		this.globals.load(new LuaMail());
		this.globals.load(new LuaGB18030());
		this.globals.load(new LuaLib091());
		this.globals.load(new LuaLib071());

		if (!this.require("core.base")) {
			logger.error("基础函数库 core.base 不存在，请检查...");
		}
	}

	/**
	 * 通过运行时环境构造 Lua 库查询目录
	 * <p>
	 * lua 模块名称可以以 .lua, .luc 和无后缀结尾, 例如, require('foo')
	 * 将会依次在 luapath 指定的目录中查找 foo.luc, foo.lua, foo 三个文件,
	 * 按照上面的顺序. 这样安排是为了兼容预编译的 lua 字节码模块(以 .luc 结尾),
	 * 当同一目录即存在 foo.luc, 也存在 foo.lua 时, 优先加载 foo.luc.
	 * </p>
	 *
	 * @return Lua 库查询目录
	 */
	private String getLuaPath() {
		StringBuilder builder = new StringBuilder();
		appendLuaPath(builder, "/lua");
		appendLuaPath(builder, "/lua/plugins");
		return builder.toString();
	}

	private void appendLuaPath(StringBuilder builder, String subpath) {
		File file = Runtime.fileFor(subpath);
		if (file == null || !file.exists()) {
			logger.warn("LUAPATH[{}]不存在，忽略.", subpath);
			return;
		}
		if (!file.isDirectory()) {
			logger.warn("LUAPATH[{}]不是目录，忽略.", file.getAbsolutePath());
			return;
		}
		String path = file.getAbsolutePath();
		builder.append(path).append("/?.luc;");
		builder.append(path).append("/?.lua;");
		builder.append(path).append("/?;");
	}

	/**
	 * 加载一个脚本库，这个函数通常用于加载一个在全局空间建立名字的模块，
	 * 因此这个函数不提供返回值.
	 *
	 * @param library 脚本库对象
	 */
	public void load(LuaValue library) {
		globals.load(library);
	}

	/**
	 * 检查调用脚本之后的返回值，根据各种类型进行正确和错误分类
	 * 一段脚本只有在返回 false 和非 0 整数的情况下是错误，其它都是正确，包括 nil
	 */
	private boolean checkReturn(LuaValue retval) {
		if (retval.isboolean()) {
			return retval.toboolean();
		}
		if (retval.isnil()) {
			return true;
		}
		if (retval.isint()) {
			return retval.toint() == 0;
		}
		return true;
	}

	/**
	 * 加载 lua 库
	 *
	 * @param script 库名称
	 * @return 0：成功， 其它：失败
	 */
	public boolean require(String script) {
		try {
			LuaValue ret = globals.get("require").call(script);
			if (!checkReturn(ret)) {
				logger.error("执行lua模块[{}]失败.", script);
				return false;
			}
			return true;
		} catch (LuaError e) {
			logger.error("执行lua模块[{}]错误[{}].", script, e.getMessage());
			return false;
		}
	}

	/**
	 * 执行脚本文件
	 *
	 * @param filename 脚本文件名称
	 * @return true / false
	 */
	public LuaValue eval(String filename) {
		if (filename.startsWith("/")) {
			return eval(new File(filename));
		}
		File file = Runtime.fileFor(filename);
		if (file == null) {
			logger.error("找不到脚本文件[{}].", filename);
			return LuaValue.NIL;
		}
		return eval(file);
	}

	/**
	 * 执行脚本文件
	 *
	 * @param file 文件对象
	 * @return 如果脚本返回 false 或者非 0 值表示失败，其它表示成功(包括nil)
	 */
	public LuaValue eval(File file) {
		String path = file.getAbsolutePath();
		long begin_time = System.currentTimeMillis();
		logger.debug("开始执行脚本[{}].", path);

		try {
			LuaValue chunk = globals.load(new FileInputStream(file),
					file.getName(), "bt", globals);
			Varargs result = chunk.invoke();
			return result.arg(1);
		} catch (FileNotFoundException e) {
			logger.error("未找到文件[{}].", path);
			return LuaValue.NIL;
		} catch (LuaError e) {
			logger.catching(e);
			logger.error("执行脚本[{}]错误，请根据日志分析错误原因.", path);
			WebNotify.error(String.format("脚本[%s]抛出异常，请检查日志", path));
			return LuaValue.NIL;
		} finally {
			elapsed_time = System.currentTimeMillis() - begin_time;
			double elapse_second = (double) elapsed_time / 1000;
			logger.debug("脚本[{}]执行完毕，耗时[{}]秒.", path, elapse_second);
		}
	}

	/**
	 * 执行一段脚本字符串
	 *
	 * @param script
	 *            字符串脚本代码
	 * @return 如果脚本返回 false 或者非 0 值表示失败，其它表示成功(包括nil)
	 */
	public LuaValue evalString(String script) {
		String abbr = script;
		if (script.length() > 20) {
			abbr = script.substring(0, 20) + " ...";
		}
		long begin_time = System.currentTimeMillis();
		logger.debug("开始执行脚本[{}].", abbr);

		try {
			LuaValue chunk = globals.load(script, "Unnamed", globals);
			Varargs result = chunk.invoke();
			return result.arg(1);
		} catch (LuaError e) {
			logger.catching(e);
			logger.error("执行脚本[{}]错误，请根据日志分析错误原因.", abbr);
			return LuaValue.NIL;
		} finally {
			elapsed_time = System.currentTimeMillis() - begin_time;
			double elapse_second = (double) elapsed_time / 1000;
			logger.debug("脚本[{}]执行完毕，耗时[{}]秒.", abbr, elapse_second);
		}
	}

	/**
	 * 取脚本执行时间，毫秒
	 *
	 * @return 毫秒
	 */
	public long getElapsedTime() {
		return this.elapsed_time;
	}

	/**
	 * 在 Lua 环境中增加一个值
	 *
	 * @param variable
	 *            句号分隔的名称，例如 reqt.v = '3'
	 * @param value
	 *            新值
	 */
	public void set(String variable, LuaValue value) {
		LuaValue env = globals;
		String[] strv = variable.split("\\.");
		for (int i = 0; i < strv.length; i++) {
			String name = strv[i];
			if (i == strv.length - 1) {
				env.set(name, value);
			} else {
				LuaValue table = env.get(name);
				if (table.isnil()) {
					table = LuaTable.tableOf();
					env.set(name, table);
				}
				env = table;
			}
		}
	}

	/**
	 * 设置字符串值
	 *
	 * @param variable
	 *            见上
	 * @param value
	 *            字符串值
	 */
	public void set(String variable, String value) {
		if (value == null) {
			return;
		}
		this.set(variable, LuaValue.valueOf(value));
	}

	/**
	 * 设置整数值
	 *
	 * @param variable
	 *            见上
	 * @param value
	 *            整数值
	 */
	public void set(String variable, int value) {
		this.set(variable, LuaValue.valueOf(value));
	}

	/**
	 * 设置用户数据
	 */
	public void set(String variable, Object userdata) {
		this.set(variable, LuaValue.userdataOf(userdata));
	}

	/**
	 * 从 Lua 环境中获取一个值.
	 *
	 * @param variable
	 *            变量名
	 * @return LuaValue 对象，可以表达各种类型的值，包括 Userdata
	 */
	public LuaValue get(String variable) {
		try {
			LuaValue env = globals;
			LuaValue result = null;
			String path = null;
			String[] strv = variable.split("\\.");
			for (int i = 0; i < strv.length; i++) {
				if (path == null) {
					path = strv[i];
				} else {
					path += "." + strv[i];
				}
				result = env.get(strv[i]);
				if (result.isnil()) {
					return LuaValue.NIL;
				}
				if (i < strv.length - 1 && !result.istable()) {
					logger.debug("变量[{}]应该是一个 table，而不是[{}].",
							path, result.typename());
					return LuaValue.NIL;
				}
				env = result;
			}
			return result;
		} catch (LuaError e) {
			logger.catching(e);
			return LuaValue.NIL;
		}
	}

	/**
	 * 加载插件
	 * 插件是普通 Lua 文件，存放在 plugins 子目录中，with 一些特定的约定.
	 *  1. 文件名必须以 plug_ 作为前缀，这个约定的原因是可以在 plugins
	 *     目录中保存一些不想干的文件，例如需要临时禁用某个插件时，只需要改个名称即可
	 *  2. 必须实现一些预定义的接口，不是所有插件需要实现所有接口，如果某个接口没有实现，
	 *     则系统不会作为错误处理，这样做的好处是，插件规范可以定义非常多的接口，
	 *     单个插件只需要根据自身的用途选择性的进行实现即可，并且将来规范中增加新的插件接口时，
	 *     插件实现不用修改
	 */
	public boolean loadPlugins(File path) {
		if (path == null) {
			path = Runtime.fileFor("/lua/plugins");
		}
		if (!path.exists()) {
			logger.warn("Lua插件目录[{}]不存在.", path.getAbsolutePath());
			return false;
		}
		File[] files = path.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.startsWith("plug_") && name.endsWith(".lua");
			}
		});
		LuaTable plugins = LuaTable.tableOf();
		for (File file : files) {
			try {
				LuaValue plugin = this.globals.loadfile(file.getAbsolutePath());
				if (!plugin.istable()) {
					logger.error("Lua插件[{}]返回值不是[table]，无效的插件.");
					continue;
				}
				plugins.insert(plugins.length() + 1, plugin);
			} catch (LuaError e) {
				logger.catching(e);
				logger.error("加载插件[{}]错误[{}].", file.getAbsolutePath(),
						e.getMessage());
			}
		}
		return true;
	}

}
